<template>
  <div
    class="SpherePanorama"
    ref="sphereContainer"
    :style="{
      width: props.widthSize ? `${props.widthSize}px` : '100%',
      height: props.heightSize ? `${props.heightSize}px` : `${defaultHeight}`,
    }"
  ></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, PropType } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'

interface PixelHotspot {
  x: number
  y: number
}

// 暴露给父组件canvas、controls、camera
const getCanvas = () => {
  return renderer?.domElement || null
}

const getControls = () => {
  return controls
}

const getCamera = () => {
  return camera
}

// 组件外部可配置的纹理/热点/尺寸等
const props = defineProps({
  texture: {
    type: String,
    required: true,
  },
  hotspot: {
    type: Object as PropType<PixelHotspot | null>,
    default: null,
  },
  widthSize: {
    type: Number,
    default: 0,
  },
  heightSize: {
    type: Number,
    default: document.documentElement.clientHeight,
  },
  showHotspot: {
    type: Boolean,
    default: true,
  },
})

const emit = defineEmits(['canvas-ready'])

// 运行期的核心 three.js 资源引用
const sphereContainer = ref<HTMLDivElement | null>(null)
const cssRenderer = ref<CSS2DRenderer | null>(null)
const animationId = ref<number | null>(null)
const textureDimensions = ref({ width: 4096, height: 2048 })
const defaultHeight = document.documentElement.clientHeight

const SPHERE_RADIUS = 80
const FOV_MIN = 25
const FOV_MAX = 40
const FOV_STEP = 2
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let renderer: THREE.WebGLRenderer | null = null
let controls: OrbitControls | null = null
let sphereMesh: THREE.Mesh<THREE.SphereGeometry, THREE.MeshBasicMaterial> | null = null
let hotspotLabel: CSS2DObject | null = null
let currentTexture: THREE.Texture | null = null
let hotspotCanvasEl: HTMLCanvasElement | null = null
let hasAlignedInitialView = false
let axesHelper: THREE.AxesHelper | null = null

// 统一的纹理加载器（允许跨域），供单图全景复用
const textureLoader = new THREE.TextureLoader()
textureLoader.crossOrigin = 'anonymous'
const loadTexture = (url: string) => {
  return new Promise<THREE.Texture>((resolve, reject) => {
    textureLoader.load(
      url,
      (texture) => {
        texture.colorSpace = THREE.SRGBColorSpace
        resolve(texture)
      },
      undefined,
      (error) => reject(error)
    )
  })
}

// 将传入的贴图挂载到翻转球体上，并更新热点/初始视角
const applyTextureToSphere = async () => {
  if (!scene) return
  try {
    const texture = await loadTexture(props.texture)
    const image = texture.image as HTMLImageElement | undefined
    if (image?.width && image?.height) {
      textureDimensions.value = { width: image.width, height: image.height }
    }
    if (!sphereMesh) {
      const geometry = new THREE.SphereGeometry(SPHERE_RADIUS, 64, 32)
      geometry.scale(-1, 1, 1)
      const material = new THREE.MeshBasicMaterial({ map: texture })
      sphereMesh = new THREE.Mesh(geometry, material)
      scene.add(sphereMesh)
    } else {
      const material = sphereMesh.material
      material.map?.dispose()
      material.map = texture
      material.needsUpdate = true
    }

    currentTexture = texture
    updateHotspotLabel()
    alignCameraToHotspot()
  } catch (error) {
    console.error('加载单张全景图失败', error)
  }
}

// 初次或强制时，将相机放在热点反方向并看向原点
const alignCameraToHotspot = (force = false) => {
  if (!scene || !camera || !controls || !props.hotspot) return
  if (hasAlignedInitialView && !force) return
  const hotspotPos = computeHotspotPosition()
  if (!hotspotPos) return
  // hotspot 朝向的单位向量（即摄像机应面向的方向）
  const direction = hotspotPos.clone().normalize()
  // 距离取热点长度的 80%，同时限制在球内，避免穿透
  const cameraDistance = Math.max(5, Math.min(SPHERE_RADIUS - 5, hotspotPos.length() * 0.8))
  const cameraPos = direction.clone().multiplyScalar(-cameraDistance)
  camera.position.copy(cameraPos)
  camera.lookAt(hotspotPos)
  controls.target.set(0, 0, 0)
  controls.update()
  hasAlignedInitialView = true
}

const computeHotspotPosition = () => {
  const { width, height } = textureDimensions.value
  if (!width || !height || !props.hotspot) return null
  const { x, y } = props.hotspot
  // 像素坐标 → UV → 球面角
  const u = THREE.MathUtils.clamp(x / width, 0, 1)
  const v = THREE.MathUtils.clamp(y / height, 0, 1)
  const yaw = u * Math.PI * 2 - Math.PI
  const pitch = Math.PI / 2 - v * Math.PI
  const radius = SPHERE_RADIUS - 0.5
  const pos = new THREE.Vector3()
  pos.x = radius * Math.cos(pitch) * Math.sin(yaw)
  pos.y = radius * Math.sin(pitch)
  pos.z = radius * Math.cos(pitch) * Math.cos(yaw)
  return pos
}

// 使用 CSS2D + canvas 绘制高亮热点
const updateHotspotLabel = () => {
  if (!scene) return
  if (hotspotLabel) {
    scene.remove(hotspotLabel)
    hotspotLabel.element.remove()
    hotspotLabel = null
  }
  if (!props.showHotspot || !props.hotspot) return
  const position = computeHotspotPosition()
  if (!position) return
  const hotspotDom = document.createElement('div')
  hotspotDom.className = 'sphere-hotspot'
  hotspotDom.title = `热点像素: (${props.hotspot.x}, ${props.hotspot.y})`
  const canvas = document.createElement('canvas')
  canvas.width = 96
  canvas.height = 96
  const ctx = canvas.getContext('2d')
  if (ctx) {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.lineWidth = 6
    ctx.strokeStyle = '#ff6600'
    ctx.shadowColor = 'rgba(255, 165, 0, 0.85)'
    ctx.shadowBlur = 12
    ctx.beginPath()
    ctx.arc(canvas.width / 2, canvas.height / 2, 30, 0, Math.PI * 2)
    ctx.stroke()
  }
  hotspotCanvasEl = canvas
  hotspotDom.appendChild(canvas)
  const cssObject = new CSS2DObject(hotspotDom)
  cssObject.position.copy(position)
  scene.add(cssObject)
  hotspotLabel = cssObject
}

// 浏览器尺寸变化时重算投影及渲染区域
const handleResize = () => {
  if (!camera || !renderer || !cssRenderer.value || !sphereContainer.value) return
  const container = sphereContainer.value
  camera.aspect = container.clientWidth / container.clientHeight
  camera.updateProjectionMatrix()
  renderer.setSize(container.clientWidth, container.clientHeight)
  cssRenderer.value.setSize(container.clientWidth, container.clientHeight)
}

// 标准 RAF 渲染循环
const animate = () => {
  if (!scene || !camera || !renderer || !controls || !cssRenderer.value) return
  animationId.value = requestAnimationFrame(animate)
  controls.update()
  renderer.render(scene, camera)
  cssRenderer.value.render(scene, camera)
}

// 自定义滚轮缩放范围（OrbitControls 的 zoom 被禁用）
const handleWheelForFov = (event: WheelEvent) => {
  if (!camera || !controls) return
  event.preventDefault()
  const delta = event.deltaY < 0 ? -FOV_STEP : FOV_STEP
  camera.fov = THREE.MathUtils.clamp(camera.fov + delta, FOV_MIN, FOV_MAX)
  camera.updateProjectionMatrix()
  // 手动触发 controls 的 change 事件，使得父组件能捕获滚轮缩放操作
  // @ts-ignore - dispatchEvent 存在于 OrbitControls 上
  controls.dispatchEvent({ type: 'change' })
}

// 初始化完整 three.js 管线，并在 ready 时告知父组件
const initPanorama = async () => {
  if (!sphereContainer.value) return
  const container = sphereContainer.value
  container.innerHTML = ''
  hasAlignedInitialView = false

  scene = new THREE.Scene()
  camera = new THREE.PerspectiveCamera(
    50,
    container.clientWidth / container.clientHeight,
    0.1,
    1000
  )
  camera.position.set(0, 0, 0.1)

  axesHelper = new THREE.AxesHelper(SPHERE_RADIUS * 1.2)
  scene.add(axesHelper)

  renderer = new THREE.WebGLRenderer({ antialias: true })
  renderer.setSize(container.clientWidth, container.clientHeight)
  renderer.setPixelRatio(window.devicePixelRatio)
  container.appendChild(renderer.domElement)
  renderer.domElement.addEventListener('wheel', handleWheelForFov, { passive: false })

  controls = new OrbitControls(camera, renderer.domElement)
  controls.enableZoom = false
  controls.enablePan = false
  controls.rotateSpeed = 1
  controls.target.set(0, 0, 0)
  controls.update()

  cssRenderer.value = new CSS2DRenderer()
  cssRenderer.value.setSize(container.clientWidth, container.clientHeight)
  cssRenderer.value.domElement.style.position = 'absolute'
  cssRenderer.value.domElement.style.top = '0'
  cssRenderer.value.domElement.style.left = '0'
  cssRenderer.value.domElement.style.pointerEvents = 'none'
  container.appendChild(cssRenderer.value.domElement)

  await applyTextureToSphere()
  window.addEventListener('resize', handleResize)
  animate()
  emit('canvas-ready')
  requestAnimationFrame(() => alignCameraToHotspot(true))
}

const disposeResources = () => {
  if (animationId.value) {
    cancelAnimationFrame(animationId.value)
    animationId.value = null
  }
  window.removeEventListener('resize', handleResize)
  renderer?.domElement.removeEventListener('wheel', handleWheelForFov)
  controls?.dispose()
  renderer?.dispose()
  cssRenderer.value?.domElement.remove()
  cssRenderer.value = null
  if (sphereMesh) {
    sphereMesh.geometry.dispose()
    sphereMesh.material.map?.dispose()
    sphereMesh.material.dispose()
    scene?.remove(sphereMesh)
    sphereMesh = null
  }
  if (hotspotLabel) {
    scene?.remove(hotspotLabel)
    hotspotLabel.element.remove()
    hotspotLabel = null
  }
  if (hotspotCanvasEl) {
    hotspotCanvasEl.remove()
    hotspotCanvasEl = null
  }
  if (axesHelper) {
    scene?.remove(axesHelper)
    axesHelper = null
  }
  currentTexture?.dispose()
  currentTexture = null
  scene = null
  camera = null
  renderer = null
  controls = null
}

onMounted(() => {
  nextTick(() => {
    initPanorama()
  })
})

onUnmounted(() => {
  disposeResources()
})

// 纹理/热点/显示开关变化时刷新展示
watch(
  () => props.texture,
  async () => {
    if (scene) {
      await applyTextureToSphere()
    }
  }
)

watch(
  () => props.hotspot,
  () => {
    updateHotspotLabel()
    alignCameraToHotspot(true)
  },
  { deep: true }
)

watch(
  () => props.showHotspot,
  () => {
    updateHotspotLabel()
  }
)

defineExpose({ getCanvas, getControls, getCamera })
</script>

<style scoped>
.SpherePanorama {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.sphere-hotspot {
  padding: 6px;
  border-radius: 50%;
  background: transparent;
  pointer-events: auto;
  cursor: pointer;
  user-select: none;
}

.sphere-hotspot canvas {
  display: block;
  width: 72px;
  height: 72px;
}
</style>
